Android 控件架构与自定义控件(四)

时间不会辜负每一个平静努力的人!

欢迎来到周建的博客: 共同致力于技术分享与交流

Android 控件架构与自定义控件(一)
Android 控件架构与自定义控件(二)
Android 控件架构与自定义控件(三)
Android 控件架构与自定义控件(四)

自定义ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。因此,自定义ViewGroup通常需要重写onMeasure() 方法来对子View进行测量,重onLayout() 方法来确定子View的位置,重写onTouchEvent() 方法增加响应事件。

实现一个Android原生控件ScrollView 的自定义ViewGroup,自定义ViewGroup可以实现ScrollView所具有的上下滑动的功能,但是在滑动的过程中,增加一个粘性效果,即当一个子view向上滑动大于一定的距离后,松开手指,它将自动向上滑动,显示下一个View。同理,如果滑动距离小于一定的距离,松开手指,它将自动滑动到开始的位置

MyScrollView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package kotlindemo.zhoujian.com.customeview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;
/**
* Created by zhoujian on 2017/6/17.
*/
public class MyScrollView extends ViewGroup
{
private int mScreenHeight;
private Scroller mScroller;
private int mLastY;
private int mStart;
private int mEnd;
public MyScrollView(Context context) {
super(context);
initView(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context)
{
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenHeight = dm.heightPixels;
mScroller = new Scroller(context);
}
/**
* 使用遍历的方式来通知子View对自身进行测量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++)
{
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
//设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
mlp.height = mScreenHeight*childCount;
setLayoutParams(mlp);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if(child.getVisibility()!= View.GONE){
child.layout(l, i * mScreenHeight,
r, (i + 1) * mScreenHeight);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int dy = mLastY - y;
if (getScrollY() < 0) {
dy = 0;
}
if (getScrollY() > getHeight() - mScreenHeight) {
dy = 0;
}
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
int dScrollY = checkAlignment();
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY);
} else {
mScroller.startScroll(
0, getScrollY(),
0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY);
} else {
mScroller.startScroll(
0, getScrollY(),
0, -mScreenHeight - dScrollY);
}
}
break;
}
postInvalidate();
return true;
}
private int checkAlignment() {
int mEnd = getScrollY();
boolean isUp = ((mEnd - mStart) > 0) ? true : false;
int lastPrev = mEnd % mScreenHeight;
int lastNext = mScreenHeight - lastPrev;
if (isUp) {
//向上的
return lastPrev;
} else {
return -lastNext;
}
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
}

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<kotlindemo.zhoujian.com.customeview.MyScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@mipmap/test1"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@mipmap/test2"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@mipmap/test3"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@mipmap/test4"/>
</kotlindemo.zhoujian.com.customeview.MyScrollView>
</LinearLayout>

显示效果:

这里写图片描述

事件拦截机制分析

一个技术总监- ViewGroupA,最外层的ViewGroup

一个项目经理 - ViewGroupB,中间的ViewGroup

一个干活的程序员 - View,最底层的View

这里写图片描述

ViewGroup 的分发事件有三个方法:

dispatchTouchEvent(MotionEvent ev);
onInterceptTouchEvent(MotionEvent ev);
onTouchEvent(MotionEvent event);

View的分发事件只有两个方法:

dispatchTouchEvent(MotionEvent ev);
onTouchEvent(MotionEvent event);

事件的传递顺序是:

技术总监(ViewGroupA) -> 项目经理 (ViewGroupB) -> 程序员(View)。事件传递的时候,先执行dispatchTouchEvent()方法,再执行onInterceptTouchEvent()方法。

true:拦截 false:不拦截,继续传递

事件处理的顺序是:

程序员(View)-> 项目经理 (ViewGroupB) -> 技术总监(ViewGroupA)。事件处理执行onTouchEvent()方法

true:表示自己处理
false:自己不处理,交给上级处理